feat: KnowledgeBase Enhancements + Knowledge Ingestion flow + Polymorphic Job Tracking - #11541
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughEnable knowledge bases feature flag. Replace knowledge creation button with dropdown menu offering File/Folder options. Restructure knowledge base columns: rename Name to Source (sortable), remove embedding model column, replace Words/Characters columns with Type/Owner, rename Avg Chunks to Avg Chunk Size, add Status column. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 4❌ Failed checks (1 error, 2 warnings, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/components/KnowledgeBasesTab.tsx`:
- Around line 200-205: The two DropdownMenuItem entries both call
handleCreateKnowledge with no distinction; update the calls to pass an explicit
type (e.g., 'file' and 'folder') and modify the handleCreateKnowledge function
signature to accept that parameter and branch on it to choose the correct
creation flow/template (or, if placeholder behavior is intended, add a clear
TODO comment next to both DropdownMenuItem entries indicating that
differentiation is pending). Ensure any TypeScript types for
handleCreateKnowledge (and any callers) are updated accordingly and that
branching uses the passed value to select the proper "Knowledge Ingestion"
example or a folder creation template.
In
`@src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx`:
- Around line 33-48: The grid columns in knowledgeBaseColumns.tsx reference
params.data.type, params.data.owner, and params.data.status but those properties
are missing from the KnowledgeBaseInfo interface; update the KnowledgeBaseInfo
interface in use-get-knowledge-bases.ts to include type, owner, and status with
appropriate types (or, if the backend doesn't supply them, remove the
corresponding columns from knowledgeBaseColumns.tsx or map existing API fields
into these properties before passing rows to the grid) so the valueGetters
return real data instead of the placeholder "—".
| <DropdownMenuItem onClick={handleCreateKnowledge}> | ||
| File | ||
| </DropdownMenuItem> | ||
| <DropdownMenuItem onClick={handleCreateKnowledge}> | ||
| Folder | ||
| </DropdownMenuItem> |
There was a problem hiding this comment.
Both "File" and "Folder" options invoke the same handler with no differentiation.
Both DropdownMenuItem elements call handleCreateKnowledge without any parameter to distinguish between File and Folder creation. The underlying handleCreateKnowledge function always uses the same "Knowledge Ingestion" example template regardless of which option is selected.
If this is intentional placeholder behavior, consider adding a TODO comment. Otherwise, pass a parameter to differentiate the action:
Suggested approach
-const handleCreateKnowledge = async () => {
+const handleCreateKnowledge = async (sourceType: "file" | "folder") => {
const knowledgeBasesExample = examples.find(
(example) => example.name === "Knowledge Ingestion",
);
+ // TODO: Use sourceType to configure the flow appropriately- <DropdownMenuItem onClick={handleCreateKnowledge}>
+ <DropdownMenuItem onClick={() => handleCreateKnowledge("file")}>
File
</DropdownMenuItem>
- <DropdownMenuItem onClick={handleCreateKnowledge}>
+ <DropdownMenuItem onClick={() => handleCreateKnowledge("folder")}>
Folder
</DropdownMenuItem>🤖 Prompt for AI Agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/components/KnowledgeBasesTab.tsx`
around lines 200 - 205, The two DropdownMenuItem entries both call
handleCreateKnowledge with no distinction; update the calls to pass an explicit
type (e.g., 'file' and 'folder') and modify the handleCreateKnowledge function
signature to accept that parameter and branch on it to choose the correct
creation flow/template (or, if placeholder behavior is intended, add a clear
TODO comment next to both DropdownMenuItem entries indicating that
differentiation is pending). Ensure any TypeScript types for
handleCreateKnowledge (and any callers) are updated accordingly and that
branching uses the passed value to select the proper "Knowledge Ingestion"
example or a folder creation template.
| headerName: "Type", | ||
| field: "type", | ||
| flex: 1, | ||
| sortable: false, | ||
| editable: false, | ||
| cellClass: baseCellClass, | ||
| valueFormatter: (params) => formatNumber(params.value), | ||
| valueGetter: (params) => params.data.type || "—", | ||
| }, | ||
| { | ||
| headerName: "Characters", | ||
| field: "characters", | ||
| headerName: "Owner", | ||
| field: "owner", | ||
| flex: 1, | ||
| sortable: false, | ||
| editable: false, | ||
| cellClass: baseCellClass, | ||
| valueFormatter: (params) => formatNumber(params.value), | ||
| valueGetter: (params) => params.data.owner || "—", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify that type, owner, and status fields exist on KnowledgeBaseInfo interface
echo "=== Searching for KnowledgeBaseInfo type definition ==="
rg -n 'type KnowledgeBaseInfo|interface KnowledgeBaseInfo' --type ts -A 20
echo ""
echo "=== Check the knowledgeBaseColumns file itself for imports ==="
head -30 src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx
echo ""
echo "=== Search for KnowledgeBaseInfo in the entire codebase ==="
rg -l 'KnowledgeBaseInfo' --type tsRepository: langflow-ai/langflow
Length of output: 6672
🏁 Script executed:
#!/bin/bash
# Read the full knowledgeBaseColumns.tsx file to verify all field references
echo "=== Full knowledgeBaseColumns.tsx file ==="
cat -n src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx
echo ""
echo "=== Full KnowledgeBaseInfo interface ==="
cat -n src/frontend/src/controllers/API/queries/knowledge-bases/use-get-knowledge-bases.ts | head -30Repository: langflow-ai/langflow
Length of output: 3774
Add type, owner, and status fields to the KnowledgeBaseInfo interface or remove these columns.
The columns at lines 33-49 and 68-76 reference params.data.type, params.data.owner, and params.data.status, but these fields do not exist on the KnowledgeBaseInfo interface defined in src/frontend/src/controllers/API/queries/knowledge-bases/use-get-knowledge-bases.ts. This will cause these columns to always display the placeholder "—". Either extend the interface and ensure the API returns these fields, or remove these columns from the grid.
🤖 Prompt for AI Agents
In
`@src/frontend/src/pages/MainPage/pages/filesPage/config/knowledgeBaseColumns.tsx`
around lines 33 - 48, The grid columns in knowledgeBaseColumns.tsx reference
params.data.type, params.data.owner, and params.data.status but those properties
are missing from the KnowledgeBaseInfo interface; update the KnowledgeBaseInfo
interface in use-get-knowledge-bases.ts to include type, owner, and status with
appropriate types (or, if the backend doesn't supply them, remove the
corresponding columns from knowledgeBaseColumns.tsx or map existing API fields
into these properties before passing rows to the grid) so the valueGetters
return real data instead of the placeholder "—".
Codecov Report❌ Patch coverage is ❌ Your project check has failed because the head coverage (49.51%) is below the target coverage (55.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #11541 +/- ##
==========================================
- Coverage 35.20% 34.21% -1.00%
==========================================
Files 1521 1453 -68
Lines 72923 69890 -3033
Branches 10936 10051 -885
==========================================
- Hits 25674 23911 -1763
+ Misses 45854 44730 -1124
+ Partials 1395 1249 -146
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…mprove UI layout - Remove .title() transformation from knowledge base names in API endpoints - Add textTransform: none to knowledge base name column in grid - Improve source chunks page layout with proper overflow handling - Enhance chunk card UI with badges, better spacing, and copy feedback - Add pagination controls with first/last page buttons and page number input - Preserve original chunk indices when filtering - Fix whit
| total_characters += int(text_series.str.len().sum()) | ||
| total_words += int(text_series.str.split().str.len().sum()) |
There was a problem hiding this comment.
⚡️Codeflash found 215% (2.15x) speedup for calculate_text_metrics in src/backend/base/langflow/api/utils/kb_helpers.py
⏱️ Runtime : 14.1 milliseconds → 4.47 milliseconds (best of 286 runs)
📝 Explanation and details
Brief: The optimized version removes expensive pandas string-accessor work and large temporary pandas objects by converting the series to a plain NumPy array of Python strings and doing the len()/split() work in tight Python loops. That cuts pandas overhead and allocations, reducing runtime from 14.1 ms to 4.47 ms (~3.15× faster; reported 214% speedup).
What changed
- Replaced two pandas .str operations per column (text_series.str.len().sum() and text_series.str.split().str.len().sum()) with:
- text_series.to_numpy() to get an ndarray of Python strings, and
- generator expressions sum(len(s) for s in arr) and sum(len(s.split()) for s in arr).
- Kept astype(str).fillna("") to preserve semantics (None/nan become the literal strings), so behavior remains unchanged.
Why this is faster
- Pandas .str accessor allocates intermediate Series/arrays and does nontrivial bookkeeping for each vectorized call. In the original code each column triggered multiple .str operations (multiple passes and temporary structures), which the profiler shows dominated runtime (large percentages on .str.len() and .str.split()).
- Converting to a NumPy array once (to_numpy()) avoids per-operation pandas overhead. Iterating Python strings with built-in len() and str.split() is very cheap compared with the cost of allocating and manipulating pandas Series objects and the lists produced by .str.split().
- Reduced object churn: .str.split() would produce Python lists or Series-of-lists, causing extra allocations. The optimized code performs the splits and length calculations in-place on the existing strings, avoiding those intermediate allocations.
- The profiler confirms this: the heavy lines in the original (.str operations) shrink substantially in the optimized run, while the cheaper Python-level loops take a small fraction of time.
Behavior and trade-offs
- Behavior is preserved: astype(str).fillna("") is still used, so None/nan/string handling remains the same and all tests pass.
- Memory: to_numpy() creates an ndarray of object references (not duplicating the full string contents), which is a small allocation compared to the savings from avoiding large temporary Series objects. For very small DataFrames, the difference is negligible; for larger ones (seen in annotated tests like 1000 rows) the benefit grows.
- Remaining hotspot: astype(str).fillna("") still shows as a significant cost in the profiler; if further speedup is required, you can explore bulk conversion strategies (e.g., processing subsets, avoiding unnecessary conversions, or using a single df[text_columns].astype(str) call) but that’s outside this change’s scope.
When this optimization helps most
- Dataframes with many rows and large text columns (the "large_scale_1000_rows" and similar annotated tests) — these show the biggest wins because they eliminate repeated pandas allocations and vectorized-access overhead.
- Little benefit for tiny data where pandas overhead is already small relative to total runtime.
Summary
- Key win: avoid repeated pandas .str accessor work and temporary Series/list allocations by doing a single to_numpy() + efficient Python iteration per column.
- Result: same behavior, substantially lower runtime (14.1 ms → 4.47 ms measured), and much lower per-column overhead in realistic, row-heavy workloads.
✅ Correctness verification report:
| Test | Status |
|---|---|
| ⚙️ Existing Unit Tests | ✅ 12 Passed |
| 🌀 Generated Regression Tests | ✅ 18 Passed |
| ⏪ Replay Tests | 🔘 None Found |
| 🔎 Concolic Coverage Tests | 🔘 None Found |
| 📊 Tests Coverage | 100.0% |
⚙️ Click to see Existing Unit Tests
🌀 Click to see Generated Regression Tests
import pandas as pd # used to construct DataFrame instances
# imports
import pytest # used for our unit tests
from langflow.api.utils.kb_helpers import calculate_text_metrics
def test_empty_dataframe_returns_zero():
# An entirely empty DataFrame with no columns should produce zero counts.
df = pd.DataFrame() # real DataFrame instance with no columns and no rows
total_words, total_chars = calculate_text_metrics(df, ["any_column"])
def test_basic_single_column_simple():
# Basic functionality: single text column with typical strings.
df = pd.DataFrame(
{
"text": [
"hello world", # 2 words, 11 characters (includes the space)
"test", # 1 word, 4 characters
"", # 0 words, 0 characters
]
}
)
# Only the 'text' column is measured
words, chars = calculate_text_metrics(df, ["text"])
def test_multiple_columns_and_missing_column():
# Verify function handles multiple text columns and skips missing ones.
df = pd.DataFrame(
{
"a": ["x y", "z"], # 'x y' -> 2 words, 3 chars; 'z' -> 1 word, 1 char
"b": [100, 200], # numeric values will be astyped to '100', '200'
}
)
# Include a non-existent column name 'missing' which should be ignored.
words, chars = calculate_text_metrics(df, ["a", "missing", "b"])
def test_text_columns_empty_list_returns_zero_even_with_data():
# If text_columns is empty, no columns are processed even if DataFrame has data.
df = pd.DataFrame({"col": ["a b c", "d e"]})
words, chars = calculate_text_metrics(df, [])
def test_nan_and_none_behavior_counts_as_strings():
# This test documents the actual behavior: the function astypes to str before fillna,
# which means None and NaN become the literal strings 'None' and 'nan' respectively,
# and are therefore counted as words and characters.
df = pd.DataFrame(
{
"c": [
None, # astype(str) -> 'None' => 1 word, 4 chars
float("nan"), # astype(str) -> 'nan' => 1 word, 3 chars
"", # '' -> 0 words, 0 chars
"nan", # literal 'nan' -> 1 word, 3 chars
"None", # literal 'None' -> 1 word, 4 chars
]
}
)
words, chars = calculate_text_metrics(df, ["c"])
def test_duplicate_columns_are_counted_multiple_times():
# If the same column name appears more than once in text_columns,
# the implementation iterates and will double-count that column.
df = pd.DataFrame({"t": ["a b", "c"]}) # words 2 + 1 = 3, chars 3 + 1 = 4
# Provide the same column twice; expected result should be doubled.
words, chars = calculate_text_metrics(df, ["t", "t"])
def test_non_string_types_are_converted_to_str_and_counted():
# Ensure different non-string types are converted to their string representations
# and then counted for words and characters as the function does via astype(str).
mixed = [123, 45.6, True, ["list"], {"k": "v"}]
df = pd.DataFrame({"mixed": mixed})
words, chars = calculate_text_metrics(df, ["mixed"])
# Compute expected by applying Python's str conversion to each element, mirroring astype(str)
expected_words = sum(len(str(x).split()) for x in mixed)
expected_chars = sum(len(str(x)) for x in mixed)
def test_large_scale_correctness_with_1000_rows():
# Large-scale test: construct 1000 rows and multiple columns to ensure scalability
n = 1000
col1 = [("a" * (i % 10)) for i in range(n)] # varying lengths 0..9, each non-empty -> 1 word
# create repeated 'b' tokens with spaces, stripped so split() yields exact counts
col2 = [(" ".join(["b"] * (i % 5))) for i in range(n)]
# create a column with up to 6 tokens
col3 = [(" ".join(["x"] * (i % 7))) for i in range(n)]
df = pd.DataFrame({"c1": col1, "c2": col2, "c3": col3})
# Compute expected results by using the same logic as the implementation:
# convert to str (they already are strings), then use .split() and len()
expected_words = 0
expected_chars = 0
for col in ["c1", "c2", "c3"]:
series = df[col].astype(str) # mirrors function behavior
# sum of character counts
expected_chars += int(series.str.len().sum())
# sum of words per row
expected_words += int(series.str.split().str.len().sum())
# Use the function and assert results match the expected computation
words, chars = calculate_text_metrics(df, ["c1", "c2", "c3", "nonexistent_col"])
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import pandas as pd # real class constructor for DataFrame
# imports
import pytest # used for our unit tests
from langflow.api.utils.kb_helpers import calculate_text_metrics
def test_basic_single_column_counts_words_and_characters():
# Create a simple DataFrame with one text column using the real pandas DataFrame constructor
df = pd.DataFrame({"text": ["hello world", "foo"]})
# Call the function under test
words, chars = calculate_text_metrics(df, ["text"])
def test_multiple_columns_and_ignores_missing_columns():
# DataFrame with two text columns and one numeric column
df = pd.DataFrame(
{
"a": ["one two", "three"],
"b": ["x y z", ""],
"num": [1, 2], # numeric column should not be counted unless listed
}
)
# Include an extra non-existent column name 'missing' which should be skipped silently
words, chars = calculate_text_metrics(df, ["a", "b", "missing"])
def test_empty_dataframe_and_empty_text_columns_returns_zeros():
# Empty DataFrame with no columns
df_empty = pd.DataFrame()
# When text_columns is empty
w1, c1 = calculate_text_metrics(df_empty, [])
# When text_columns contains names not present in the DataFrame: they should be ignored
w2, c2 = calculate_text_metrics(df_empty, ["nonexistent", "also_missing"])
def test_none_and_nan_and_numeric_values_are_cast_to_strings_and_counted():
# The implementation calls astype(str) first, so None -> 'None' and nan -> 'nan'
df = pd.DataFrame({"t": [None, float("nan"), 123]})
words, chars = calculate_text_metrics(df, ["t"])
def test_whitespace_and_special_characters_counted_correctly():
# Strings with newlines, tabs, and multiple spaces
df = pd.DataFrame(
{
"s": [
"a\nb\tc d", # a newline and a tab and double space -> 4 words
"\tleading and trailing \n", # words: "leading","and","trailing" -> 3 words
]
}
)
words, chars = calculate_text_metrics(df, ["s"])
# Count words explicitly to make the assertion clear and robust
expected_words = sum(len(str(val).split()) for val in df["s"])
expected_chars = sum(len(str(val)) for val in df["s"])
def test_large_scale_1000_rows_multiple_columns():
# Construct 1000 rows to validate scaling up to the requested size
n_rows = 1000
# Column a: 5 occurrences of 'word ' (with trailing space); split() will count 5 words
# Column b: single token of 10 'x' characters -> 1 word per row
col_a = [("word " * 5) for _ in range(n_rows)] # each entry length = 5 * len("word ") = 25 chars
col_b = [("x" * 10) for _ in range(n_rows)] # each entry length = 10 chars
df = pd.DataFrame({"a": col_a, "b": col_b})
words, chars = calculate_text_metrics(df, ["a", "b"])
def test_order_of_text_columns_does_not_change_result():
# Ensure different ordering of columns in the list yields the same totals
df = pd.DataFrame({"c1": ["one two"], "c2": ["three four five"]})
codeflash_output = calculate_text_metrics(df, ["c1", "c2"]); res1 = codeflash_output
codeflash_output = calculate_text_metrics(df, ["c2", "c1"]); res2 = codeflash_output
def test_mixed_type_columns_handled_and_counted_as_strings():
df = pd.DataFrame({"m": [100, 200.5, "text"]})
words, chars = calculate_text_metrics(df, ["m"])
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.To test or edit this optimization locally git merge codeflash/optimize-pr11541-2026-02-26T14.38.38
| total_characters += int(text_series.str.len().sum()) | |
| total_words += int(text_series.str.split().str.len().sum()) | |
| # Use a single pass over the string values to avoid multiple pandas string-method allocations. | |
| arr = text_series.to_numpy() | |
| total_characters += int(sum(len(s) for s in arr)) | |
| total_words += int(sum(len(s.split()) for s in arr)) |
There was a problem hiding this comment.
Addressed the review comments.
…entations into more testable blocks, refactored helpers into class base helpers, fixed code smells, added exhautive unit tests, extracted constant values into a separate file.
Code Review - Remaining ItemsIMPORTANT — Must fix before merge1. Internal errors exposed to end users (
|
| Category | Count | Status |
|---|---|---|
| CRITICAL (Blockers) | 0 | All resolved |
| IMPORTANT (Must fix) | 3 | Items 1, 2, 3 |
| RECOMMENDED | 2 | Items 4, 5 |
| TESTING gaps | 2 | Items 6, 7 |
Resolving the 3 IMPORTANT items unblocks approval.
|
|
||
| class KBIngestionHelper: | ||
| """Helper class for Knowledge Base ingestion processes.""" | ||
|
|
||
| @staticmethod | ||
| async def perform_ingestion( | ||
| kb_name: str, | ||
| kb_path: Path, |
There was a problem hiding this comment.
⚡️Codeflash found 899% (8.99x) speedup for KBAnalysisHelper._get_text_columns in src/backend/base/langflow/api/utils/kb_helpers.py
⏱️ Runtime : 12.6 milliseconds → 1.26 milliseconds (best of 99 runs)
📝 Explanation and details
Brief: The optimized version speeds up membership and dtype checks by moving repeated, Python-level work into O(1) set lookups and a single, vectorized pandas call. This removes expensive per-column Python work and uses faster C-level pandas routines where possible — resulting in ~10x runtime improvement (12.6ms -> 1.26ms).
What changed
- Schema branch: build cols_set = set(df.columns) once and use it for membership checks instead of repeatedly checking col in df.columns.
- Common-name branch: convert common_names into a set (common_set) so the col.lower() membership test is O(1) instead of scanning a small list each time.
- Fallback branch: replace the Python loop [col for col in df.columns if df[col].dtype == "object"] with pandas' vectorized df.select_dtypes(include=["object"]).columns and cast to list.
Why these changes are faster
- set membership is average O(1) vs repeated index/list membership which is O(n) or O(log n) for Index.contains; when schema_data or df.columns is large, switching to sets reduces many lookups from costly operations to cheap ones.
- The original fallback did a Python-level iteration accessing df[col].dtype for every column. That is relatively expensive due to attribute access and per-Series work. select_dtypes is implemented in pandas/Cython and performs dtype selection much more efficiently in bulk, removing Python per-column overhead.
- Converting common names to a set eliminates repeated linear scans of the small list, which helps when df has many columns (col.lower() in common_set is O(1) rather than O(k) per column).
- The line profiler confirms the hot spots moved: the original spent almost all time in the per-column dtype loop; the optimized version pushes that work into select_dtypes and uses sets for membership, dramatically reducing total time.
Behavioral/compatibility notes
- The functional behavior is preserved: schema-priority logic, case-insensitive common-name detection, and the final fallback (select only object-dtype columns) remain the same. Using select_dtypes(include=["object"]) matches the original intent of selecting columns with dtype == "object".
- Memory overhead: creating small sets is negligible compared to the performance benefits. If this function is called repeatedly on the same DataFrame in a tight loop, you could micro-optimize further by caching df.columns as a set outside the function.
Which workloads benefit most
- Large schema_data and/or DataFrames with many columns (see annotated tests test_large_schema_search_performance_and_correctness, test_large_dataframe_object_dtype_returns_all_object_columns, test_large_scale_* cases). These tests show the biggest wins because they trigger many membership/dtype checks.
- Small DataFrames still benefit but the relative gain is smaller.
Summary
- Replaced repeated Python-level per-column operations with O(1) set lookups and a vectorized pandas call.
- This reduces algorithmic cost of the hot paths and leverages optimized pandas internals, producing the observed ~10x speedup while preserving behavior expected by the regression tests.
✅ Correctness verification report:
| Test | Status |
|---|---|
| ⚙️ Existing Unit Tests | ✅ 12 Passed |
| 🌀 Generated Regression Tests | ✅ 20 Passed |
| ⏪ Replay Tests | 🔘 None Found |
| 🔎 Concolic Coverage Tests | 🔘 None Found |
| 📊 Tests Coverage | 100.0% |
⚙️ Click to see Existing Unit Tests
🌀 Click to see Generated Regression Tests
import pandas as pd # used to construct DataFrame inputs
# imports
import pytest # used for our unit tests
from langflow.api.utils.kb_helpers import KBAnalysisHelper
def test_schema_prioritized_columns_present():
# Create a DataFrame with several columns including 'a' which the schema will mark as text
df = pd.DataFrame({"a": ["hello"], "b": [1], "text": ["x"]})
# Schema marks 'a' as vectorize=True and data_type='string'; also include a missing column
schema = [
{"column_name": "a", "vectorize": True, "data_type": "string"},
{"column_name": "missing", "vectorize": True, "data_type": "string"},
]
# Expect the helper to return only 'a' because it's the only schema-declared text column present in df
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema); result = codeflash_output
def test_schema_ignored_when_not_vectorize_or_not_string_and_common_name_matches():
# DataFrame contains a common-name column 'Content' (mixed-case)
df = pd.DataFrame({"id": [1], "Content": ["hello world"], "other": [2]})
# Schema exists but does not mark any valid text columns (vectorize=False or wrong data_type). Should fall back.
schema = [
{"column_name": "id", "vectorize": False, "data_type": "string"},
{"column_name": "other", "vectorize": False, "data_type": "integer"},
]
# With no effective schema text columns, the helper should detect 'Content' by case-insensitive common names
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema); result = codeflash_output
def test_common_name_detection_multiple_order_preserved():
# DataFrame with multiple common-name columns in a particular order and mixed casing
df = pd.DataFrame({"Text": ["a"], "alpha": [0], "CONTENT": ["b"], "Document": ["c"], "z": [1]})
# No schema_data provided -> should match common names in the DataFrame column order
codeflash_output = KBAnalysisHelper._get_text_columns(df); result = codeflash_output
def test_fallback_to_object_dtype_when_no_schema_and_no_common_names():
# Create DataFrame with one object dtype column ('col1') and one numeric column ('col2')
df = pd.DataFrame({"col1": ["s1", "s2"], "col2": [1, 2]})
# No schema and no common-name columns -> should return columns whose dtype is object
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
def test_empty_dataframe_returns_empty_list():
# Empty DataFrame with no columns
df = pd.DataFrame()
# Neither schema nor columns -> should return empty list
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
def test_schema_columns_not_in_df_falls_back_to_common_names():
# Schema points to a column not present in df; df contains 'chunk' so should be detected via common names
df = pd.DataFrame({"chunk": ["part1"], "other": [1]})
schema = [{"column_name": "not_present", "vectorize": True, "data_type": "string"}]
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=schema); result = codeflash_output
def test_large_schema_search_performance_and_correctness():
# Build a large DataFrame with 1000 columns; include a special target column 'col_500' that the schema will mark
data = {f"col_{i}": [f"v{i}"] for i in range(1000)}
df = pd.DataFrame(data)
# Create a large schema: most entries are not vectorized, except one that should be selected
schema = [
{"column_name": f"col_{i}", "vectorize": False, "data_type": "string"} for i in range(1000)
]
# Mark a single column as vectorize True and data_type 'string'
schema[500] = {"column_name": "col_500", "vectorize": True, "data_type": "string"}
# The helper should return only the single column marked for vectorization and present in df
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=schema); result = codeflash_output
def test_large_dataframe_object_dtype_returns_all_object_columns():
# Construct a DataFrame with 1000 object-typed columns (strings)
n = 1000
data = {f"obj_{i}": [f"v{i}"] for i in range(n)}
df = pd.DataFrame(data)
# No schema and no common-name columns; should return all columns because all are object dtype
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import pandas as pd # DataFrame construction and dtypes
# imports
import pytest # used for our unit tests
from langflow.api.utils.kb_helpers import KBAnalysisHelper
def test_schema_data_prefers_vectorized_string_columns_basic():
# Create a DataFrame with some columns present
df = pd.DataFrame(
{
"a": [1], # numeric column
"text_col": ["hello"], # object/string column
"b": ["x"], # object/string column
}
)
# schema_data lists three columns; two exist in df, one does not.
schema_data = [
{"column_name": "b", "vectorize": True, "data_type": "string"},
{"column_name": "a", "vectorize": True, "data_type": "string"},
{"column_name": "missing", "vectorize": True, "data_type": "string"},
]
# Expect the function to return the intersection of schema order with df.columns:
# order preserved from schema list but only existing columns kept -> ['b','a']
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output
def test_schema_data_ignores_non_vectorized_or_non_string_entries():
# Construct a DataFrame with two columns
df = pd.DataFrame({"c": ["one"], "text": ["two"]})
# schema_data includes a valid entry for 'c' and two invalid entries
schema_data = [
{"column_name": "c", "vectorize": True, "data_type": "string"},
{"column_name": "text", "vectorize": False, "data_type": "string"}, # vectorize False -> ignore
{"column_name": "text", "vectorize": True, "data_type": "int"}, # not string -> ignore
]
# Only 'c' should be selected from schema_data
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output
def test_schema_data_with_entries_not_in_dataframe_returns_empty_list_no_fallback():
# When schema_data yields text_columns but none are in df.columns, the function returns []
df = pd.DataFrame({"Content": ["alpha"], "other": ["beta"]})
# schema_data contains valid vectorize/string entries but none of these columns exist in df
schema_data = [
{"column_name": "not_here", "vectorize": True, "data_type": "string"},
{"column_name": "also_missing", "vectorize": True, "data_type": "string"},
]
# Because text_columns from schema_data is non-empty, the code will attempt to filter them by df.columns
# and return the filtered list (which should be empty) instead of falling back to common names.
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output
def test_common_names_detection_case_insensitive_and_preserve_df_order():
# Create DataFrame with common names in mixed case and specific order
df = pd.DataFrame(
{
"id": [1],
"Document": ["doc1"],
"Text": ["txt1"],
"other": ["o"],
"chunk": ["c"], # also a common name (lowercase)
}
)
# No schema_data provided, so common name detection should run.
# It should be case-insensitive and preserve the order of df.columns.
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
def test_fallback_to_object_dtype_only_includes_object_series():
# Column 'b' has dtype object (default for Python strings), 'c' has pandas string dtype
df = pd.DataFrame(
{
"a": [1, 2], # int -> not object
"b": ["x", "y"], # object dtype (default)
"c": pd.Series(["s1", "s2"], dtype="string"), # pandas StringDtype, not 'object'
}
)
# No schema_data and no common names -> final fallback should return only columns with dtype == 'object'
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
def test_empty_dataframe_returns_empty_list():
# Empty DataFrame with no columns
df_empty = pd.DataFrame()
# No schema_data -> should return empty list
codeflash_output = KBAnalysisHelper._get_text_columns(df_empty, schema_data=None); result = codeflash_output
# If schema_data is an empty list (falsy), behavior is same as None -> empty DataFrame still returns []
codeflash_output = KBAnalysisHelper._get_text_columns(df_empty, schema_data=[]); result2 = codeflash_output
def test_schema_data_none_and_empty_list_behave_same_as_no_schema():
# DataFrame with a common-name column to confirm fallback path works when schema_data is None or empty list
df = pd.DataFrame({"content": ["a"], "other": ["b"]})
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); res_none = codeflash_output
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=[]); res_empty = codeflash_output
def test_non_string_object_dtype_values_still_count_as_object():
# Column 'obj' will contain dictionaries; dtype should be 'object' and therefore be selected by fallback
df = pd.DataFrame({"obj": [{"k": 1}, {"k": 2}], "num": [1, 2]})
codeflash_output = KBAnalysisHelper._get_text_columns(df); res = codeflash_output
def test_large_scale_schema_and_dataframe_intersection_preserves_schema_order():
# Build a large DataFrame with 1000 columns named col0..col999
n = 1000
data = {}
for i in range(n):
# Use small lists for values; dtype will be object because they are Python strings
data[f"col{i}"] = [f"v{i}"]
df = pd.DataFrame(data)
# Build schema_data of length 1000 where every even-indexed column is vectorize True/string
schema_data = []
expected = []
# Put some names not in df as well to ensure filtering works
for i in range(n):
entry = {
"column_name": f"col{i}" if (i % 3 != 0) else f"missing_{i}", # every 3rd is missing
"vectorize": (i % 2 == 0), # even indices vectorize True
"data_type": "string" if (i % 5 != 0) else "int", # some are not strings
}
schema_data.append(entry)
# Determine expected: column must be vectorize True, data_type 'string', and exist in df
if entry["vectorize"] and entry["data_type"] == "string" and entry["column_name"] in df.columns:
expected.append(entry["column_name"])
# Call the helper and verify it returns the expected intersection in schema order
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data); result = codeflash_output
def test_large_scale_common_names_detection_in_big_dataframe():
# Build a large DataFrame with 1000 columns but include a few common-name columns scattered
n = 1000
cols = [f"col{i}" for i in range(n)]
# Insert some common names at specific positions
cols[10] = "Text"
cols[200] = "content"
cols[999] = "Document"
# Create DataFrame with these columns
df = pd.DataFrame({c: [f"value_{i}"] for i, c in enumerate(cols)})
# No schema_data -> should detect the three common-name columns in df order
codeflash_output = KBAnalysisHelper._get_text_columns(df, schema_data=None); result = codeflash_output
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.To test or edit this optimization locally git merge codeflash/optimize-pr11541-2026-02-26T21.08.57
| class KBIngestionHelper: | |
| """Helper class for Knowledge Base ingestion processes.""" | |
| @staticmethod | |
| async def perform_ingestion( | |
| kb_name: str, | |
| kb_path: Path, | |
| cols_set = set(df.columns) | |
| return [col for col in text_columns if col in cols_set] | |
| common_names = ["text", "content", "document", "chunk"] | |
| common_set = set(common_names) | |
| text_columns = [col for col in df.columns if col.lower() in common_set] | |
| if text_columns: | |
| return text_columns | |
| return list(df.select_dtypes(include=["object"]).columns) |
# Conflicts: # src/frontend/package-lock.json # src/lfx/src/lfx/_assets/component_index.json
LE-207
Frontend Work:
@deon-sanchez
Testing Steps for QA
KNOWLEDGE BASES FEATURE — QA TEST PLAN.pdf
Backend Work:
@dkaushik94
Description
This PR introduces a significant refactor of the Knowledge Bases infrastructure alongside generalizations to our Job/Task service and backend safety protocols. The implementations resolve existing data contention issues while enabling cleaner background task handling and paginated results.
🚀 Features & Enhancements
Knowledge Base Architecture & Storage Refactor
TaskServicedeployments with tracking managed by internalJobService. Supports both status polling and dynamic job cancellation.Read-Onlyexceptions and lock contention by generating a forced fresh Chroma persistent client prior to active storage allocation and ensuring resources are properly garbage-collected upon teardowns.avg_chunk_size,words, andsource_types, effectively caching them locally to speed up/knowledge_bases/fetching loops by drastically omitting repetitive folder iterations.Job and Task Service Generalizations
job_type,asset_id, andasset_typecolumns to allow polymorphism. This grants jobs the elasticity to process Canvas evaluations, Datasets mapping, and KB ingestions agnostically without blocking primary router loops.🛠️ Refactoring & Code Quality
HTTPExceptions. Raw exception objects are no longer leaked directly to users; issues are safely logged locally (logger.aerror()) and users only receive controlled standard text formats globally, addressing blind exception catching.f-stringformats strictly to comply with ruff formatUP031and corrected file-length guidelines.test_knowledge_bases_apivastly to fully test edge-case states (zero-file uploads, missing dimensions, missing metadata recovery, ingestion rollback).🐛 Additional Fixes
_get_text_columnsfallback bypass which previously forcedavg_chunk_sizecalculations to universally output 0.0.